package calao.storage.bdb;

import static calao.util.Logger.*;

import java.util.HashMap;

import com.sleepycat.bind.tuple.TupleInput;
import com.sleepycat.je.Database;
import com.sleepycat.je.Environment;

import calao.storage.DataBinding;
import calao.storage.Field;
import calao.storage.UnknownBindingException;
import calao.util.Printable;

import com.sleepycat.je.Cursor;
import com.sleepycat.je.DatabaseConfig;
import com.sleepycat.je.DatabaseException;
import com.sleepycat.je.DatabaseEntry;
import com.sleepycat.je.LockMode;
import com.sleepycat.je.OperationStatus;
import com.sleepycat.je.Sequence;
import com.sleepycat.je.SequenceConfig;
import java.io.PrintWriter;

public class EnvRegistry 
implements Printable
{

	final static byte IDSEQ_KEY = 1;

	final static byte FIELD_KEY = 2;	

	final static byte CLASS_KEY = 3;

	final Database db;
	final Sequence idSeq;

	private final HashMap<Field,Integer> fieldIdMap = new HashMap<Field,Integer>();
	private final HashMap<Integer,Field> fieldMap = new HashMap<Integer,Field>();

	private final HashMap<DataBinding,Integer> bindingIdMap = new HashMap<DataBinding,Integer>();
	private final HashMap<Integer,DataBinding> bindingMap = new HashMap<Integer,DataBinding>();
	
	private final HashMap<Integer,String> orphanMap = new HashMap<Integer,String>();

	EnvRegistry(Environment env)
	throws DatabaseException
	{
		DatabaseConfig config = new DatabaseConfig();
		config.setAllowCreate(true);
		db = env.openDatabase(null, "registry", config);
		SequenceConfig seqConfig = new SequenceConfig();
		seqConfig.setRange(1, Integer.MAX_VALUE);
		seqConfig.setInitialValue(1);
		seqConfig.setAllowCreate(true);
		idSeq = db.openSequence(null, Util.toEntry(IDSEQ_KEY, null), seqConfig);
		load();
	}

	private void load()
	throws DatabaseException
	{
		Cursor cursor = null;
		DatabaseEntry key = Util.toEntry(CLASS_KEY, null); 
		DatabaseEntry data = new DatabaseEntry();
		try {
			for (;;) {
				OperationStatus status = null;
				if (cursor==null) {
					cursor = db.openCursor(null, null);
					status = cursor.getSearchKeyRange(key, data, LockMode.DEFAULT);
				}
				else {
					status = cursor.getNext(key, data, LockMode.DEFAULT);
				}
				if (status!=OperationStatus.SUCCESS) break;
				TupleInput input = Util.toInput(key);
				int marker = input.readByte();
				if (marker!=CLASS_KEY) break;
				String string = input.readString();
				int id = Util.toInt(data);
				orphanMap.put(id, string);
			}
		}
		finally {
			if (cursor!=null) cursor.close();
		}
	}

	private int getStringId(String string, byte marker)
	throws DatabaseException
	{
		int id;
		DatabaseEntry data = new DatabaseEntry();
		OperationStatus status = db.get(null, Util.toEntry(marker, string), 
				data, LockMode.DEFAULT);
		if (status==OperationStatus.SUCCESS) {
			id = Util.toInt(data); 
			loaded(string, id);
		}
		else {
			id = (int)idSeq.get(null, 1);
			db.put(null, Util.toEntry(marker, string), Util.toEntry(id));
			registered(string, id);
		}
		return id;
	}

	private void loaded(String string, int id)
	{
		log(DEBUG, "Loaded registered string %d: \"%s\"", id, string);
	}

	private void registered(String string, int id)
	{
		log(DEBUG, "Registered string \"%s\" as %d", string, id);		
	}

	int getId(Field field)
	throws DatabaseException
	{
		Integer id = fieldIdMap.get(field);
		if (id==null) {
			String string = field.getTable().getName()+"#"+field.getName();
			id = getStringId(string, FIELD_KEY);
			fieldMap.put(id, field);
			fieldIdMap.put(field, id);
		}
		return id;
	}

	Field getField(int id) 
	{
		return fieldMap.get(id);
	}

	int getId(DataBinding binding) 
	throws DatabaseException
	{
		Integer id = bindingIdMap.get(binding);
		if (id==null) {
			id = getStringId(binding.getName(), CLASS_KEY);
			bindingMap.put(id, binding);
			bindingIdMap.put(binding, id);
		}
		return id;
	}

	DataBinding getDataBinding(int id)
	throws UnknownBindingException
	{
		DataBinding binding = bindingMap.get(id);
		if (binding==null) {
			String name = orphanMap.get(id);
			if (name==null) throw new UnknownBindingException();
			binding = DataBinding.getBinding(name);
			if (binding==null) throw new UnknownBindingException(name);
			orphanMap.remove(name);
			bindingMap.put(id, binding);
			bindingIdMap.put(binding, id);
			loaded(name, id);
		}
		return binding;
	}

	void close()
	{
		try {
			db.close();
		}
		catch (Exception e) {
			log(e);
		}
		fieldIdMap.clear();
		fieldMap.clear();
	}

	public void printTo(PrintWriter writer) 
	{
		Cursor cursor = null;
		try {
			cursor = db.openCursor(null, null);
			DatabaseEntry key = new DatabaseEntry();
			DatabaseEntry data = new DatabaseEntry();
			OperationStatus status = null;
			for (;;) {
				status = status==null? 
						cursor.getFirst(key, data, LockMode.DEFAULT)
						: cursor.getNext(key, data, LockMode.DEFAULT);
						if (status!=OperationStatus.SUCCESS) break;
						TupleInput in = Util.toInput(key);
						switch (in.readByte()) {
						case FIELD_KEY:
							writer.format("field key %d=%s", Util.toInt(data), in.readString());
							break;
						case CLASS_KEY:
							writer.format("class key %d=%s", Util.toInt(data), in.readString());
							break;
						default:
							continue;
						}
						writer.println();
			}
		}
		catch (DatabaseException e) {
			throw new RuntimeException(e);
		}
		finally {
			try {
				if (cursor!=null) cursor.close();
			}
			catch (DatabaseException e) {
				log(e);
			}
		}
	}

}
